#!/usr/bin/env python
# -*- coding:utf-8 -*-

'''
PACKET SPECIFICATION:
    packet ::= route_slice separator data_slice
    separator ::= '\t'

    route_slice         ::= action_field service_field id_field

    action_field        ::= 'action:' action_name
    action_name         ::= 'REQUEST' | 'RESPONSE' | 'FORWARD'

    service_field       ::= 'service:' service_name
    service_name        ::= letter (letter | digit)*

    id_field            ::= packet_id_field [respone_id_field]
    packet_id_field     ::= 'packid:' decimalinteger
    response_id_field   ::= 'respid:' decimalinteger

    data_slice ::= 'body:' data
    data       ::= base64_encode_string

    identifier ::=  (letter|"_") (letter | digit | "_")*
    letter     ::=  lowercase | uppercase
    lowercase  ::=  "a"..."z"
    uppercase  ::=  "A"..."Z"
    digit      ::=  "0"..."9"

    decimalinteger ::=  nonzerodigit digit* | "0"
    nonzerodigit   ::=  "1"..."9"

'''

import os, sys, signal
import socket, threading, time
import base64

from jujuc import jujucore, jujuchat

def DEBUG(format, *args) : print format % args

#######################################################################

will_stop = False

def signal_stop( signo, frame ) :
    global  will_stop
    #DEBUG( "JUJUB catch #%d signal, would stop.", signo )
    will_stop = True
    return
    # signal_stop


#######################################################################

TERMINATOR = '\n'

MIN_PACKET_ID = 1
MAX_PACKET_ID = 65535
packet_id_current = MAX_PACKET_ID

PP_ACTION_REQUEST  = 'REQUEST'
PP_ACTION_RESPONSE = 'RESPONSE'


class   Packet(object)  :
    def __init__(self) :
        self.action = None
        self.service = None
        self.body = None
        self.packid = 0
        self.respid = 0
        # __init__()

    def __str__(self) :
        s = "action:%s service:%s packid:%d" % (self.action, self.service, self.packid)
        if self.respid > 0 :
            s += " respid:%d" % self.respid
        s += " body:'%s'" % self.body
        return s
        # __str__()

    def isComplete(self) :
        if not self.action or not self.service or not self.body :
            return  False
        return  True

    def setRequest(self) :
        if PP_ACTION_REQUEST == self.action : return
        self.action = PP_ACTION_REQUEST
    def setResponse(self) :
        if PP_ACTION_RESPONSE == self.action : return
        self.action = PP_ACTION_RESPONSE

    def isRequest(self) :
        if PP_ACTION_REQUEST == self.action : return True
        return  False
    def isResponse(self) :
        if PP_ACTION_RESPONSE == self.action : return True
        return  False
    # class Packet

def depack(record) :
    '''decode packet record string into Packet struct'''
    field_separator = '\t'
    kv_separator = ':'
    packet = Packet()
    fields = record.split(field_separator)
    for f in fields :
        (key, value) = f.split(kv_separator, 1)
        if not value : continue
        if 'action' == key :
            packet.action = value
        elif 'service' == key :
            packet.service = value
        elif 'packid' == key :
            packet.packid = int(value)
        elif 'respid' == key :
            packet.respid = int(value)
        elif 'body' == key :
            packet.body = base64.b64decode(value)

    if not packet.isComplete() : return None
    return  packet
    # depack()

def enpack(packet) :
    '''encode Packet struct into packet record string'''
    packid = packet.packid
    if not packid :  packid = nextPacketID()
    record = "action:%s\tservice:%s\tpackid:%d" % (packet.action, packet.service, packid)
    if packet.respid > 0 :
        record += "\trespid:%d" % packet.respid
    record += "\tbody:%s" % base64.b64encode(packet.body)
    return  (record, packid)
    # enpack

def nextPacketID() :
    global  packet_id_current
    if packet_id_current == MAX_PACKET_ID :
        packet_id_current = MIN_PACKET_ID
    else :
        packet_id_current += 1
    return  packet_id_current
    # packetid()

#######################################################################

class   Context(object) :
    '''Context -- context for Consumer'''
    def __init__(self, dispatcher) :
        self.dispatcher = dispatcher
        # __init__()
    # class Context

#######################################################################

class   Server(object) :
    def serve(self, service, request, address_from) :
        '''do some service on request

        response, address_to = serve(PyString service, PyString request, PyString address_from)

        @param service: name of service
        @param request: request to be handled. may be need decode/unpack first, as to application
        @param address_from: where request come from

        @return: a two-tuple of the response and address_to. If no response, return None
        '''
        raise NotImplementedError
        # serve()


class   Dispatcher(object)  :
    '''Dispatcher -- dispatch message to consumer'''
    def __init__(self) :
        # table-address : address --> channel
        self.__table_address = {}
        # table-channel : channel --> state of channel
        self.__table_channels = {}
        # table-consumer : SERVICE --> server
        self.__table_services = {}
        # __init__()

    def forward(self, channel_from, record) :
        '''forward a record to a channel
        '''
        pass
        # forward()

    def dispatch(self, channel_from, record) :
        '''dispatch a record to a consumer

        List of Actions
         * parse record to get address info (such as SERVICE name)
         * lookup route-table to get the Consumer who is responsible for the record,
            if No Consumer is responsible for it :
                discard the message(record)
                return False
            OR
            if No Consumer is responsible for it :
                use Router as Consumer (route the message again)
         * feed message to consumer
         * return True
        '''
        # parse record, to get SERVICE
        DEBUG('CHANNEL %s RECV %s', channel_from.address, record)
        packet = depack(record)
        if not packet : raise RuntimeError, 'invalid record: %s' % record

        if packet.isRequest() :
            service = packet.service
            # find Consumer for the SERVICE
            if service not in self.__table_services : return  False
            #    for each SERVICE, organize Consumers as a list, we just use the first consumer
            server = self.__table_services[service][0]
            # just ignore, or log it to some where if NOT ok
            response, address_to = server.serve(service, packet.body, channel_from.address)
            if response is None :
                # No response
                return True
            packet.body = response
            packet.respid = packet.packid
            packet.packid = 0
            packet.setResponse()
            record, response_packid = enpack(packet)
            self.passon(record, address_to)
        elif packet.isResponse() :
            if channel_from not in self.__table_channel : return True
            service, producer, consumer, queue_out, queue_err = self.__table_channel[channel_from][0]
            if consumer is None : return True
            if service != packet.service : return False
            ok = consumer.feed(packet, channel_from, self)
            # for RESPONSE consumer
            if packet.respid in queue_out :
                if ok :
                    # erase packet from sending queue
                    del queue_out[ packet.respid ]
                else :
                    # move packet from sending-queue to error-queue
                    queue_err[ packet.respid ] = queue_out[ packet.respid ]
                    del queue_out[ packet.respid ]
            else :
                # suspicious packet response id
                pass
        else :
            raise RuntimeError, 'cannot handle %s action' % packet.action

        return  True
        # dispatch()

    def passon(self, record, address) :
        '''pass packet on a channel by address

        todo : what to do if no channal registered for address???
        '''
        channel = self.lookupChannel(address)
        if channel is None :
            raise RuntimeError, 'No channel labeled %s' % address
        channel.push(record + TERMINATOR)
        # passon()

    ###################################################################

    def registerService(self, service, server) :
        if not service : raise RuntimeError, 'Dispatcher: invalid service to register'
        if not server : raise RuntimeError, 'Dispatcher: invalid server to register'
        if service in self.__table_services :
            self.__table_services[service].insert(0, server)
        else :
            self.__table_services[service] = [server,]
        # registerService()
    def unregisterService(self, service, server=None) :
        if not service : raise RuntimeError, 'Dispatcher: invalid service to unregister'
        if not server : raise RuntimeError, 'Dispatcher: invalid server to unregister'
        if service in self.__table_services :
            if server is None :
                del self.__table_services[service]
            else :
                self.__table_services[service].remove(server)
                if len(self.__table_services[service]) < 1 :
                    # no server registered for this service any more
                    del self.__table_services[service]
        # unregisterService()

    def registerChannel(self, address, channel) :
        self.__table_address[ address ] = channel
        # registerChannel()
    def unregisterChannel(self, address) :
        if not address : return
        if address in self.__table_address :
            channel = self.__table_address[ address ]
            del self.__table_address[ address ]
        # unregisterChannel()
    def lookupChannel(self, address) :
        if address in self.__table_address :
            return self.__table_address[ address ]
        return None
        # lookupChannel()
    # class Dispatcher

#######################################################################

class   JuChannel(jujuchat.Jujuchat) :
    def    __init__(self, address, context, connect=None) :
        jujuchat.Jujuchat.__init__(self, conn=connect)
        if connect :
            self.__connect = connect
        else :
            self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
            self.connect(address)
            self.__connect = self.socket
        self.address = address
        self.__buf_incoming = []
        self.__terminator = TERMINATOR
        self.setTerminator(self.__terminator)

        self.__context = context
        self.__context.dispatcher.registerChannel(self.address, self)
        # __init__()

    def OnRecvRecord(self, record):
        self.__context.dispatcher.dispatch(self, record)
        # OnRecvRecord()

    def handle_connect(self) :
        pass
        # handle_connect()

    def handle_close(self) :
        #DEBUG('JUJUB close connection for client "%s".', self.address)
        self.__context.dispatcher.unregisterChannel(self.address)
        self.close()
        # handle_close()

    # class JuChannel

class   JuServer(jujucore.dispatcher):

    def __init__(self, port, context):
        jujucore.dispatcher.__init__(self)
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.bind(("", port))
        self.listen(5)
        #DEBUG('JUJUB listening on PORT:%d ...', port)
        self.context = context
        # __init__()

    def handle_accept(self) :
        connect, client_address = self.accept()
        #DEBUG('JUJUB accept client %s', client)

        JuChannel(client_address, self.context, connect)
        # handle_accept()
    # class JuServer


#######################################################################

class   Jujube(object) :
    def __init__(self) :
        dispatcher = Dispatcher()
        context = Context(dispatcher)
        self.__context = context
        self.__listeners = {}
        self.__transporters = {}
        # __init__()

    def listen(self, port) :
        if port in self.__listeners : return
        listener = JuServer(port, self.__context)
        self.__listeners[port] = listener
        # listen()

    def registerService(self, service, server) :
        '''register SERVICE'''
        self.__context.dispatcher.registerService(service, server)
        # registerServer()

    def unregisterService(self, service, server) :
        '''unregister SERVICE'''
        self.__context.dispatcher.unregisterService(service, server)
        # unregisterServer()

    def run(self) :
        # mask signal
        #DEBUG('os.name: %s', os.name)
        signal.signal( signal.SIGTERM, signal_stop )
        if 'posix' == os.name :
            signal.signal( signal.SIGQUIT, signal_stop )

        try :
            while True :
                if len(jujucore.socket_map) < 1 : break
                jujucore.loop(timeout=1.0, count=1)
        except KeyboardInterrupt :
            #DEBUG( "JUJUB interrupt : %s", `sys.exc_info()[1]` )
            pass
        except :
            raise
        # run()

#######################################################################


class EchoServer(Server)  :
    '''Echo Consumer'''
    def serve(self, service, record, address_from) :
        DEBUG('SERVICE "%s" echo "%s" from "%s"', service, record, address_from)
        # echo back
        address_to = address_from
        return record, address_to
        # serve()
    # class EchoServer


HOST = "127.0.0.1"
PORT = 54344

def main():
    s = Jujube()
    s.listen(PORT)

    server = EchoServer()
    s.registerService('ECHO', server)
    s.run()
    # main()

if __name__ == '__main__' :
    main()
    import sys
    sys.exit(0)

